package com.dragon.sso.ldap;

import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

@Slf4j
public class LDAPConnector {

  private static final int LDAP_VERSION = LDAPConnection.LDAP_V3;
  private String ldapHost;
  private int ldapPort;

  private String defaultBindDn;
  private String defaultBindPwd;

  private String baseDn;
  private volatile LDAPConnector instance;

  private LDAPConnector() {}

  public static synchronized LDAPConnectorBuilder builder() {
    // 初始化一个完全新的 ldapConnector
    LDAPConnector ldapConnector = new LDAPConnector();
    ldapConnector.instance = ldapConnector;
    return ldapConnector.new LDAPConnectorBuilder();
  }

  /** 修改某些现有的配置来达到重新构建 ldapConnector 的目的 */
  public LDAPConnectorBuilder copyBuilder() {
    return this.new LDAPConnectorBuilder();
  }

  @SuppressWarnings("unused")
  public class LDAPConnectorBuilder {
    private String ldapHost;
    private int ldapPort;
    private String defaultBindDn;
    private String defaultBindPwd;
    private String baseDn;

    public LDAPConnectorBuilder ldapHost(String ldapHost) {
      this.ldapHost = ldapHost;
      return this;
    }

    public LDAPConnectorBuilder ldapPort(int ldapPort) {
      this.ldapPort = ldapPort;
      return this;
    }

    public LDAPConnectorBuilder defaultBindDn(String defaultBindDn) {
      this.defaultBindDn = defaultBindDn;
      return this;
    }

    public LDAPConnectorBuilder defaultBindPwd(String defaultBindPwd) {
      this.defaultBindPwd = defaultBindPwd;
      return this;
    }

    public LDAPConnectorBuilder baseDn(String baseDn) {
      this.baseDn = baseDn;
      return this;
    }

    public LDAPConnector build() {

      if (this.ldapHost == null) {
        this.ldapHost = LDAPConnector.this.ldapHost;
      }
      if (this.ldapPort == 0) {
        this.ldapPort = LDAPConnector.this.ldapPort;
      }
      if (this.defaultBindDn == null) {
        this.defaultBindDn = LDAPConnector.this.defaultBindDn;
      }
      if (this.defaultBindPwd == null) {
        this.defaultBindPwd = LDAPConnector.this.defaultBindPwd;
      }
      if (this.baseDn == null) {
        this.baseDn = LDAPConnector.this.baseDn;
      }

      if (StringUtils.isEmpty(ldapHost)) {
        throw new RuntimeException("ldapHost 不能为空");
      }

      if (ldapPort == 0) {
        throw new RuntimeException("%s: ldapPort 不能为空");
      }

      if (StringUtils.isEmpty(defaultBindDn)) {
        throw new RuntimeException("%s: bindDn 不能为空");
      }

      if (StringUtils.isEmpty(defaultBindPwd)) {
        throw new RuntimeException("%s: defaultBindPwd 不能为空");
      }

      if (StringUtils.isEmpty(baseDn)) {
        throw new RuntimeException("baseDn 不能为空");
      }
      if (LDAPConnector.this.instance == null) {
        synchronized (LDAPConnector.this) {
          if (LDAPConnector.this.instance == null) {
            LDAPConnector.this.instance = new LDAPConnector();
          }
        }
      }

      LDAPConnector ldapConnector = LDAPConnector.this.instance;
      ldapConnector.ldapHost = ldapHost;
      ldapConnector.ldapPort = ldapPort;
      ldapConnector.defaultBindDn = defaultBindDn;
      ldapConnector.defaultBindPwd = defaultBindPwd;
      ldapConnector.baseDn = baseDn;
      return ldapConnector;
    }
  }

  public boolean verifyPassword(String username, String password) {
    return verifyPassword(username, password, null);
  }

  public boolean verifyPassword(String username, String password, Consumer<LDAPEntry> consumer) {
    boolean debugEnabled = log.isDebugEnabled();

    AtomicReference<LDAPEntry> userDnRefer = new AtomicReference<>(null);
    try {
      searchOnePerson(username, userDnRefer::set);
      LDAPEntry entry = userDnRefer.get();
      String userDn = entry.getDN();
      if (debugEnabled) {
        log.debug("{}'s DN is {}", username, userDn);
      }
      exec(lc -> bindBaseDn(lc, userDn, password), false);

      if (consumer != null) {
        consumer.accept(entry);
      }
      return true;
    } catch (LDAPException e) {
      if (e.getResultCode() == LDAPException.NO_SUCH_OBJECT) {
        log.info("Error: No such entry");
      } else if (e.getResultCode() == LDAPException.NO_SUCH_ATTRIBUTE) {
        log.info("Error: No such attribute");
      } else {
        log.info("Error: " + e);
      }
    }
    return false;
  }

  public boolean doVerifyPassword(String username, String userDn, String password) throws LDAPException {
    if (log.isDebugEnabled()) {
      log.debug("{}'s DN is {}", username, userDn);
    }
    try {
      exec(lc -> bindBaseDn(lc, userDn, password), false);
      return true;
    } catch (LDAPException e) {
      if (e.getResultCode() == LDAPException.NO_SUCH_OBJECT) {
        log.info("Error: No such entry");
      } else if (e.getResultCode() == LDAPException.NO_SUCH_ATTRIBUTE) {
        log.info("Error: No such attribute");
      } else {
        log.info("Error: " + e);
      }
    }
    return false;
  }
  public void searchAll(Consumer<LDAPSearchResults> consumer) throws LDAPException {
    String searchFilter = "objectClass=*";
    int scope = LDAPConnection.SCOPE_SUB;
    exec(
        ldapConnection ->
            consumer.accept(ldapConnection.search(baseDn, scope, searchFilter, null, false)));
  }

  public void searchOnePerson(String username, Consumer<LDAPEntry> consumer) throws LDAPException {
    String searchFilter = "(&(objectClass=organizationalPerson)(sAMAccountName=" + username + "))";
    int scope = LDAPConnection.SCOPE_SUB;
    exec(
        lc -> {
          LDAPSearchResults search = lc.search(baseDn, scope, searchFilter, null, false);
          consumer.accept(search.next());
        });
  }

  private void exec(Consumer<LDAPConnection> consumer) throws LDAPException {
    exec(consumer, true);
  }

  private void exec(Consumer<LDAPConnection> consumer, boolean bindDefaultBaseDn)
      throws LDAPException {
    LDAPConnection ldapConnection = initConnect();
    try {
      connect(ldapConnection);
      if (bindDefaultBaseDn) {
        bindBaseDn(ldapConnection);
      }
      consumer.accept(ldapConnection);
    } finally {
      try {
        ldapConnection.disconnect();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  private void connect(LDAPConnection ldapConnection) throws LDAPException {
    ldapConnection.connect(ldapHost, ldapPort);
    log.info("login ldap server successfully.");
  }

  private LDAPConnection initConnect() {
    return new LDAPConnection();
  }

  private void bindBaseDn(LDAPConnection ldapConnection) throws LDAPException {
    bindBaseDn(ldapConnection, defaultBindDn, defaultBindPwd);
  }

  private void bindBaseDn(LDAPConnection ldapConnection, String bindDn, String bindPwd)
      throws LDAPException {
    ldapConnection.bind(LDAP_VERSION, bindDn, bindPwd.getBytes(StandardCharsets.UTF_8));
  }

  @FunctionalInterface
  public interface Consumer<T> {
    void accept(T t) throws LDAPException;
  }
}
